// Copyright Epic Games, Inc. All Rights Reserved.

#include "httpprojectstore.h"

#include "projectstore.h"

#include <zencore/compactbinarybuilder.h>
#include <zencore/compactbinarypackage.h>
#include <zencore/compactbinaryutil.h>
#include <zencore/filesystem.h>
#include <zencore/fmtutils.h>
#include <zencore/logging.h>
#include <zencore/stream.h>
#include <zencore/trace.h>

namespace zen {

Oid
OpKeyStringAsOId(std::string_view OpKey)
{
	using namespace std::literals;

	CbObjectWriter Writer;
	Writer << "key"sv << OpKey;

	XXH3_128Stream KeyHasher;
	Writer.Save()["key"sv].WriteToStream([&](const void* Data, size_t Size) { KeyHasher.Append(Data, Size); });
	XXH3_128 KeyHash = KeyHasher.GetHash();

	Oid OpId;
	memcpy(OpId.OidBits, &KeyHash, sizeof(OpId.OidBits));

	return OpId;
}

void
CSVHeader(bool Details, bool AttachmentDetails, StringBuilderBase& CSVWriter)
{
	if (AttachmentDetails)
	{
		CSVWriter << "Project, Oplog, LSN, Key, Cid, Size";
	}
	else if (Details)
	{
		CSVWriter << "Project, Oplog, LSN, Key, Size, AttachmentCount, AttachmentsSize";
	}
	else
	{
		CSVWriter << "Project, Oplog, Key";
	}
}

void
CSVWriteOp(CidStore&		  CidStore,
		   std::string_view	  ProjectId,
		   std::string_view	  OplogId,
		   bool				  Details,
		   bool				  AttachmentDetails,
		   int				  LSN,
		   const Oid&		  Key,
		   CbObjectView		  Op,
		   StringBuilderBase& CSVWriter)
{
	StringBuilder<32> KeyStringBuilder;
	Key.ToString(KeyStringBuilder);
	const std::string_view KeyString = KeyStringBuilder.ToView();

	if (AttachmentDetails)
	{
		Op.IterateAttachments([&CidStore, &CSVWriter, &ProjectId, &OplogId, LSN, &KeyString](CbFieldView FieldView) {
			const IoHash AttachmentHash = FieldView.AsAttachment();
			IoBuffer	 Attachment		= CidStore.FindChunkByCid(AttachmentHash);
			CSVWriter << "\r\n"
					  << ProjectId << ", " << OplogId << ", " << LSN << ", " << KeyString << ", " << AttachmentHash.ToHexString() << ", "
					  << gsl::narrow<uint64_t>(Attachment.GetSize());
		});
	}
	else if (Details)
	{
		uint64_t AttachmentCount = 0;
		size_t	 AttachmentsSize = 0;
		Op.IterateAttachments([&CidStore, &AttachmentCount, &AttachmentsSize](CbFieldView FieldView) {
			const IoHash AttachmentHash = FieldView.AsAttachment();
			AttachmentCount++;
			IoBuffer Attachment = CidStore.FindChunkByCid(AttachmentHash);
			AttachmentsSize += Attachment.GetSize();
		});
		CSVWriter << "\r\n"
				  << ProjectId << ", " << OplogId << ", " << LSN << ", " << KeyString << ", " << gsl::narrow<uint64_t>(Op.GetSize()) << ", "
				  << AttachmentCount << ", " << gsl::narrow<uint64_t>(AttachmentsSize);
	}
	else
	{
		CSVWriter << "\r\n" << ProjectId << ", " << OplogId << ", " << KeyString;
	}
};

//////////////////////////////////////////////////////////////////////////

namespace {

	void CbWriteOp(CidStore&	   CidStore,
				   bool			   Details,
				   bool			   OpDetails,
				   bool			   AttachmentDetails,
				   int			   LSN,
				   const Oid&	   Key,
				   CbObjectView	   Op,
				   CbObjectWriter& CbWriter)
	{
		CbWriter.BeginObject();
		{
			CbWriter.AddObjectId("key", Key);
			if (Details)
			{
				CbWriter.AddInteger("lsn", LSN);
				CbWriter.AddInteger("size", gsl::narrow<uint64_t>(Op.GetSize()));
			}
			if (AttachmentDetails)
			{
				CbWriter.BeginArray("attachments");
				Op.IterateAttachments([&CidStore, &CbWriter](CbFieldView FieldView) {
					const IoHash AttachmentHash = FieldView.AsAttachment();
					CbWriter.BeginObject();
					{
						IoBuffer Attachment = CidStore.FindChunkByCid(AttachmentHash);
						CbWriter.AddString("cid", AttachmentHash.ToHexString());
						CbWriter.AddInteger("size", gsl::narrow<uint64_t>(Attachment.GetSize()));
					}
					CbWriter.EndObject();
				});
				CbWriter.EndArray();
			}
			else if (Details)
			{
				uint64_t AttachmentCount = 0;
				size_t	 AttachmentsSize = 0;
				Op.IterateAttachments([&CidStore, &AttachmentCount, &AttachmentsSize](CbFieldView FieldView) {
					const IoHash AttachmentHash = FieldView.AsAttachment();
					AttachmentCount++;
					IoBuffer Attachment = CidStore.FindChunkByCid(AttachmentHash);
					AttachmentsSize += Attachment.GetSize();
				});
				if (AttachmentCount > 0)
				{
					CbWriter.AddInteger("attachments", AttachmentCount);
					CbWriter.AddInteger("attachmentssize", gsl::narrow<uint64_t>(AttachmentsSize));
				}
			}
			if (OpDetails)
			{
				CbWriter.BeginObject("op");
				for (const CbFieldView& Field : Op)
				{
					if (!Field.HasName())
					{
						CbWriter.AddField(Field);
						continue;
					}
					std::string_view FieldName = Field.GetName();
					CbWriter.AddField(FieldName, Field);
				}
				CbWriter.EndObject();
			}
		}
		CbWriter.EndObject();
	};

	void CbWriteOplogOps(CidStore&			  CidStore,
						 ProjectStore::Oplog& Oplog,
						 bool				  Details,
						 bool				  OpDetails,
						 bool				  AttachmentDetails,
						 CbObjectWriter&	  Cbo)
	{
		Cbo.BeginArray("ops");
		{
			Oplog.IterateOplogWithKey([&Cbo, &CidStore, Details, OpDetails, AttachmentDetails](int LSN, const Oid& Key, CbObjectView Op) {
				CbWriteOp(CidStore, Details, OpDetails, AttachmentDetails, LSN, Key, Op, Cbo);
			});
		}
		Cbo.EndArray();
	}

	void CbWriteOplog(CidStore&			   CidStore,
					  ProjectStore::Oplog& Oplog,
					  bool				   Details,
					  bool				   OpDetails,
					  bool				   AttachmentDetails,
					  CbObjectWriter&	   Cbo)
	{
		Cbo.BeginObject();
		{
			Cbo.AddString("name", Oplog.OplogId());
			CbWriteOplogOps(CidStore, Oplog, Details, OpDetails, AttachmentDetails, Cbo);
		}
		Cbo.EndObject();
	}

	void CbWriteOplogs(CidStore&				CidStore,
					   ProjectStore::Project&	Project,
					   std::vector<std::string> OpLogs,
					   bool						Details,
					   bool						OpDetails,
					   bool						AttachmentDetails,
					   CbObjectWriter&			Cbo)
	{
		Cbo.BeginArray("oplogs");
		{
			for (const std::string& OpLogId : OpLogs)
			{
				ProjectStore::Oplog* Oplog = Project.OpenOplog(OpLogId);
				if (Oplog != nullptr)
				{
					CbWriteOplog(CidStore, *Oplog, Details, OpDetails, AttachmentDetails, Cbo);
				}
			}
		}
		Cbo.EndArray();
	}

	void CbWriteProject(CidStore&				 CidStore,
						ProjectStore::Project&	 Project,
						std::vector<std::string> OpLogs,
						bool					 Details,
						bool					 OpDetails,
						bool					 AttachmentDetails,
						CbObjectWriter&			 Cbo)
	{
		Cbo.BeginObject();
		{
			Cbo.AddString("name", Project.Identifier);
			CbWriteOplogs(CidStore, Project, OpLogs, Details, OpDetails, AttachmentDetails, Cbo);
		}
		Cbo.EndObject();
	}

}  // namespace

//////////////////////////////////////////////////////////////////////////

HttpProjectService::HttpProjectService(CidStore& Store, ProjectStore* Projects, HttpStatsService& StatsService, AuthMgr& AuthMgr)
: m_Log(logging::Get("project"))
, m_CidStore(Store)
, m_ProjectStore(Projects)
, m_StatsService(StatsService)
, m_AuthMgr(AuthMgr)
{
	using namespace std::literals;

	m_StatsService.RegisterHandler("prj", *this);

	m_Router.AddPattern("project", "([[:alnum:]_.]+)");
	m_Router.AddPattern("log", "([[:alnum:]_.]+)");
	m_Router.AddPattern("op", "([[:digit:]]+?)");
	m_Router.AddPattern("chunk", "([[:xdigit:]]{24})");
	m_Router.AddPattern("hash", "([[:xdigit:]]{40})");

	m_Router.RegisterRoute(
		"",
		[this](HttpRouterRequest& Req) { HandleProjectListRequest(Req); },
		HttpVerb::kGet);

	m_Router.RegisterRoute(
		"list",
		[this](HttpRouterRequest& Req) { HandleProjectListRequest(Req); },
		HttpVerb::kGet);

	m_Router.RegisterRoute(
		"{project}/oplog/{log}/batch",
		[this](HttpRouterRequest& Req) { HandleChunkBatchRequest(Req); },
		HttpVerb::kPost);

	m_Router.RegisterRoute(
		"{project}/oplog/{log}/files",
		[this](HttpRouterRequest& Req) { HandleFilesRequest(Req); },
		HttpVerb::kGet);

	m_Router.RegisterRoute(
		"{project}/oplog/{log}/chunkinfos",
		[this](HttpRouterRequest& Req) { HandleChunkInfosRequest(Req); },
		HttpVerb::kGet);

	m_Router.RegisterRoute(
		"{project}/oplog/{log}/{chunk}/info",
		[this](HttpRouterRequest& Req) { HandleChunkInfoRequest(Req); },
		HttpVerb::kGet);

	m_Router.RegisterRoute(
		"{project}/oplog/{log}/{chunk}",
		[this](HttpRouterRequest& Req) { HandleChunkByIdRequest(Req); },
		HttpVerb::kGet | HttpVerb::kHead);

	m_Router.RegisterRoute(
		"{project}/oplog/{log}/{hash}",
		[this](HttpRouterRequest& Req) { HandleChunkByCidRequest(Req); },
		HttpVerb::kGet | HttpVerb::kPost);

	m_Router.RegisterRoute(
		"{project}/oplog/{log}/prep",
		[this](HttpRouterRequest& Req) { HandleOplogOpPrepRequest(Req); },
		HttpVerb::kPost);

	m_Router.RegisterRoute(
		"{project}/oplog/{log}/new",
		[this](HttpRouterRequest& Req) { HandleOplogOpNewRequest(Req); },
		HttpVerb::kPost);

	m_Router.RegisterRoute(
		"{project}/oplog/{log}/{op}",
		[this](HttpRouterRequest& Req) { HandleOpLogOpRequest(Req); },
		HttpVerb::kGet);

	m_Router.RegisterRoute(
		"{project}/oplog/{log}",
		[this](HttpRouterRequest& Req) { HandleOpLogRequest(Req); },
		HttpVerb::kGet | HttpVerb::kPut | HttpVerb::kPost | HttpVerb::kDelete);

	m_Router.RegisterRoute(
		"{project}/oplog/{log}/entries",
		[this](HttpRouterRequest& Req) { HandleOpLogEntriesRequest(Req); },
		HttpVerb::kGet);

	m_Router.RegisterRoute(
		"{project}",
		[this](HttpRouterRequest& Req) { HandleProjectRequest(Req); },
		HttpVerb::kGet | HttpVerb::kPut | HttpVerb::kPost | HttpVerb::kDelete);

	// Push a oplog container
	m_Router.RegisterRoute(
		"{project}/oplog/{log}/save",
		[this](HttpRouterRequest& Req) { HandleOplogSaveRequest(Req); },
		HttpVerb::kPost);

	// Pull a oplog container
	m_Router.RegisterRoute(
		"{project}/oplog/{log}/load",
		[this](HttpRouterRequest& Req) { HandleOplogLoadRequest(Req); },
		HttpVerb::kGet);

	// Do an rpc style operation on project/oplog
	m_Router.RegisterRoute(
		"{project}/oplog/{log}/rpc",
		[this](HttpRouterRequest& Req) { HandleRpcRequest(Req); },
		HttpVerb::kPost);

	m_Router.RegisterRoute(
		"details\\$",
		[this](HttpRouterRequest& Req) { HandleDetailsRequest(Req); },
		HttpVerb::kGet);

	m_Router.RegisterRoute(
		"details\\$/{project}",
		[this](HttpRouterRequest& Req) { HandleProjectDetailsRequest(Req); },
		HttpVerb::kGet);

	m_Router.RegisterRoute(
		"details\\$/{project}/{log}",
		[this](HttpRouterRequest& Req) { HandleOplogDetailsRequest(Req); },
		HttpVerb::kGet);

	m_Router.RegisterRoute(
		"details\\$/{project}/{log}/{chunk}",
		[this](HttpRouterRequest& Req) { HandleOplogOpDetailsRequest(Req); },
		HttpVerb::kGet);
}

HttpProjectService::~HttpProjectService()
{
	m_StatsService.UnregisterHandler("prj", *this);
}

const char*
HttpProjectService::BaseUri() const
{
	return "/prj/";
}

void
HttpProjectService::HandleRequest(HttpServerRequest& Request)
{
	m_ProjectStats.RequestCount++;

	metrics::OperationTiming::Scope $(m_HttpRequests);

	if (m_Router.HandleRequest(Request) == false)
	{
		m_ProjectStats.BadRequestCount++;
		ZEN_WARN("No route found for {0}", Request.RelativeUri());
	}
}

void
HttpProjectService::HandleStatsRequest(HttpServerRequest& HttpReq)
{
	ZEN_TRACE_CPU("ProjectService::Stats");

	const GcStorageSize StoreSize = m_ProjectStore->StorageSize();
	const CidStoreSize	CidSize	  = m_CidStore.TotalSize();

	CbObjectWriter Cbo;

	EmitSnapshot("requests", m_HttpRequests, Cbo);

	Cbo.BeginObject("store");
	{
		Cbo.BeginObject("size");
		{
			Cbo << "disk" << StoreSize.DiskSize;
			Cbo << "memory" << StoreSize.MemorySize;
		}
		Cbo.EndObject();

		Cbo.BeginObject("project");
		{
			Cbo << "readcount" << m_ProjectStats.ProjectReadCount << "writecount" << m_ProjectStats.ProjectWriteCount << "deletecount"
				<< m_ProjectStats.ProjectDeleteCount;
		}
		Cbo.EndObject();

		Cbo.BeginObject("oplog");
		{
			Cbo << "readcount" << m_ProjectStats.OpLogReadCount << "writecount" << m_ProjectStats.OpLogWriteCount << "deletecount"
				<< m_ProjectStats.OpLogDeleteCount;
		}
		Cbo.EndObject();

		Cbo.BeginObject("op");
		{
			Cbo << "hitcount" << m_ProjectStats.OpHitCount << "misscount" << m_ProjectStats.OpMissCount << "writecount"
				<< m_ProjectStats.OpWriteCount;
		}
		Cbo.EndObject();

		Cbo.BeginObject("chunk");
		{
			Cbo << "hitcount" << m_ProjectStats.ChunkHitCount << "misscount" << m_ProjectStats.ChunkMissCount << "writecount"
				<< m_ProjectStats.ChunkWriteCount;
		}
		Cbo.EndObject();

		Cbo << "requestcount" << m_ProjectStats.RequestCount;
		Cbo << "badrequestcount" << m_ProjectStats.BadRequestCount;
	}
	Cbo.EndObject();

	Cbo.BeginObject("cid");
	{
		Cbo.BeginObject("size");
		{
			Cbo << "tiny" << CidSize.TinySize;
			Cbo << "small" << CidSize.SmallSize;
			Cbo << "large" << CidSize.LargeSize;
			Cbo << "total" << CidSize.TotalSize;
		}
		Cbo.EndObject();
	}
	Cbo.EndObject();

	return HttpReq.WriteResponse(HttpResponseCode::OK, Cbo.Save());
}

void
HttpProjectService::HandleProjectListRequest(HttpRouterRequest& Req)
{
	ZEN_TRACE_CPU("ProjectService::ProjectList");

	HttpServerRequest& HttpReq		= Req.ServerRequest();
	CbArray			   ProjectsList = m_ProjectStore->GetProjectsList();
	HttpReq.WriteResponse(HttpResponseCode::OK, ProjectsList);
}

void
HttpProjectService::HandleChunkBatchRequest(HttpRouterRequest& Req)
{
	ZEN_TRACE_CPU("ProjectService::ChunkBatch");

	HttpServerRequest& HttpReq	 = Req.ServerRequest();
	const auto&		   ProjectId = Req.GetCapture(1);
	const auto&		   OplogId	 = Req.GetCapture(2);

	Ref<ProjectStore::Project> Project = m_ProjectStore->OpenProject(ProjectId);
	if (!Project)
	{
		return HttpReq.WriteResponse(HttpResponseCode::NotFound);
	}
	Project->TouchProject();

	ProjectStore::Oplog* FoundLog = Project->OpenOplog(OplogId);
	if (!FoundLog)
	{
		return HttpReq.WriteResponse(HttpResponseCode::NotFound);
	}
	Project->TouchOplog(OplogId);

	// Parse Request

	IoBuffer	 Payload = HttpReq.ReadPayload();
	BinaryReader Reader(Payload);

	struct RequestHeader
	{
		enum
		{
			kMagic = 0xAAAA'77AC
		};
		uint32_t Magic;
		uint32_t ChunkCount;
		uint32_t Reserved1;
		uint32_t Reserved2;
	};

	struct RequestChunkEntry
	{
		Oid		 ChunkId;
		uint32_t CorrelationId;
		uint64_t Offset;
		uint64_t RequestBytes;
	};

	if (Payload.Size() <= sizeof(RequestHeader))
	{
		m_ProjectStats.BadRequestCount++;
		HttpReq.WriteResponse(HttpResponseCode::BadRequest);
	}

	RequestHeader RequestHdr;
	Reader.Read(&RequestHdr, sizeof RequestHdr);

	if (RequestHdr.Magic != RequestHeader::kMagic)
	{
		m_ProjectStats.BadRequestCount++;
		HttpReq.WriteResponse(HttpResponseCode::BadRequest);
	}

	std::vector<RequestChunkEntry> RequestedChunks;
	RequestedChunks.resize(RequestHdr.ChunkCount);
	Reader.Read(RequestedChunks.data(), sizeof(RequestChunkEntry) * RequestHdr.ChunkCount);

	// Make Response

	struct ResponseHeader
	{
		uint32_t Magic = 0xbada'b00f;
		uint32_t ChunkCount;
		uint32_t Reserved1 = 0;
		uint32_t Reserved2 = 0;
	};

	struct ResponseChunkEntry
	{
		uint32_t CorrelationId;
		uint32_t Flags = 0;
		uint64_t ChunkSize;
	};

	std::vector<IoBuffer> OutBlobs;
	OutBlobs.emplace_back(sizeof(ResponseHeader) + RequestHdr.ChunkCount * sizeof(ResponseChunkEntry));
	for (uint32_t ChunkIndex = 0; ChunkIndex < RequestHdr.ChunkCount; ++ChunkIndex)
	{
		const RequestChunkEntry& RequestedChunk = RequestedChunks[ChunkIndex];
		IoBuffer				 FoundChunk		= FoundLog->FindChunk(RequestedChunk.ChunkId);
		if (FoundChunk)
		{
			if (RequestedChunk.Offset > 0 || RequestedChunk.RequestBytes < uint64_t(-1))
			{
				uint64_t Offset = RequestedChunk.Offset;
				if (Offset > FoundChunk.Size())
				{
					Offset = FoundChunk.Size();
				}
				uint64_t Size = RequestedChunk.RequestBytes;
				if ((Offset + Size) > FoundChunk.Size())
				{
					Size = FoundChunk.Size() - Offset;
				}
				FoundChunk = IoBuffer(FoundChunk, Offset, Size);
			}
		}
		OutBlobs.emplace_back(std::move(FoundChunk));
	}
	uint8_t*	   ResponsePtr = reinterpret_cast<uint8_t*>(OutBlobs[0].MutableData());
	ResponseHeader ResponseHdr;
	ResponseHdr.ChunkCount = RequestHdr.ChunkCount;
	memcpy(ResponsePtr, &ResponseHdr, sizeof(ResponseHdr));
	ResponsePtr += sizeof(ResponseHdr);
	for (uint32_t ChunkIndex = 0; ChunkIndex < RequestHdr.ChunkCount; ++ChunkIndex)
	{
		// const RequestChunkEntry& RequestedChunk = RequestedChunks[ChunkIndex];
		const IoBuffer&	   FoundChunk(OutBlobs[ChunkIndex + 1]);
		ResponseChunkEntry ResponseChunk;
		ResponseChunk.CorrelationId = ChunkIndex;
		if (FoundChunk)
		{
			ResponseChunk.ChunkSize = FoundChunk.Size();
		}
		else
		{
			ResponseChunk.ChunkSize = uint64_t(-1);
		}
		memcpy(ResponsePtr, &ResponseChunk, sizeof(ResponseChunk));
		ResponsePtr += sizeof(ResponseChunk);
	}
	m_ProjectStats.ChunkHitCount += RequestHdr.ChunkCount;
	return HttpReq.WriteResponse(HttpResponseCode::OK, HttpContentType::kBinary, OutBlobs);
}

void
HttpProjectService::HandleFilesRequest(HttpRouterRequest& Req)
{
	ZEN_TRACE_CPU("ProjectService::Files");

	using namespace std::literals;

	HttpServerRequest& HttpReq = Req.ServerRequest();

	// File manifest fetch, returns the client file list

	const auto& ProjectId = Req.GetCapture(1);
	const auto& OplogId	  = Req.GetCapture(2);

	HttpServerRequest::QueryParams Params = HttpReq.GetQueryParams();

	std::unordered_set<std::string> WantedFieldNames;
	if (auto FieldFilter = HttpServerRequest::Decode(Params.GetValue("fieldnames")); !FieldFilter.empty())
	{
		if (FieldFilter != "*")	 // Get all - empty FieldFilter equal getting all fields
		{
			ForEachStrTok(FieldFilter, ',', [&](std::string_view FieldName) {
				WantedFieldNames.insert(std::string(FieldName));
				return true;
			});
		}
	}
	else
	{
		const bool FilterClient = Params.GetValue("filter"sv) == "client"sv;
		WantedFieldNames.insert("id");
		WantedFieldNames.insert("clientpath");
		if (!FilterClient)
		{
			WantedFieldNames.insert("serverpath");
		}
	}

	CbObject								 ResponsePayload;
	std::pair<HttpResponseCode, std::string> Result =
		m_ProjectStore->GetProjectFiles(ProjectId, OplogId, WantedFieldNames, ResponsePayload);
	if (Result.first == HttpResponseCode::OK)
	{
		if (HttpReq.AcceptContentType() == HttpContentType::kCompressedBinary)
		{
			CompositeBuffer Payload = CompressedBuffer::Compress(ResponsePayload.GetBuffer()).GetCompressed();
			return HttpReq.WriteResponse(HttpResponseCode::OK, HttpContentType::kCompressedBinary, Payload);
		}
		else
		{
			return HttpReq.WriteResponse(HttpResponseCode::OK, ResponsePayload);
		}
	}
	else
	{
		if (Result.first == HttpResponseCode::BadRequest)
		{
			m_ProjectStats.BadRequestCount++;
		}
		ZEN_DEBUG("Request {}: '{}' failed with {}. Reason: `{}`",
				  ToString(HttpReq.RequestVerb()),
				  HttpReq.QueryString(),
				  static_cast<int>(Result.first),
				  Result.second);
	}
	if (Result.second.empty())
	{
		return HttpReq.WriteResponse(Result.first);
	}
	return HttpReq.WriteResponse(Result.first, HttpContentType::kText, Result.second);
}

void
HttpProjectService::HandleChunkInfosRequest(HttpRouterRequest& Req)
{
	ZEN_TRACE_CPU("ProjectService::ChunkInfos");

	HttpServerRequest& HttpReq = Req.ServerRequest();

	const auto& ProjectId = Req.GetCapture(1);
	const auto& OplogId	  = Req.GetCapture(2);

	HttpServerRequest::QueryParams Params = HttpReq.GetQueryParams();

	std::unordered_set<std::string> WantedFieldNames;
	if (auto FieldFilter = HttpServerRequest::Decode(Params.GetValue("fieldnames")); !FieldFilter.empty())
	{
		if (FieldFilter != "*")	 // Get all - empty FieldFilter equal getting all fields
		{
			ForEachStrTok(FieldFilter, ',', [&](std::string_view FieldName) {
				WantedFieldNames.insert(std::string(FieldName));
				return true;
			});
		}
	}
	else
	{
		WantedFieldNames.insert("id");
		WantedFieldNames.insert("rawhash");
		WantedFieldNames.insert("rawsize");
	}

	CbObject								 ResponsePayload;
	std::pair<HttpResponseCode, std::string> Result =
		m_ProjectStore->GetProjectChunkInfos(ProjectId, OplogId, WantedFieldNames, ResponsePayload);
	if (Result.first == HttpResponseCode::OK)
	{
		if (HttpReq.AcceptContentType() == HttpContentType::kCompressedBinary)
		{
			CompositeBuffer Payload = CompressedBuffer::Compress(ResponsePayload.GetBuffer()).GetCompressed();
			return HttpReq.WriteResponse(HttpResponseCode::OK, HttpContentType::kCompressedBinary, Payload);
		}
		else
		{
			return HttpReq.WriteResponse(HttpResponseCode::OK, ResponsePayload);
		}
	}
	else
	{
		if (Result.first == HttpResponseCode::BadRequest)
		{
			m_ProjectStats.BadRequestCount++;
		}
		ZEN_DEBUG("Request {}: '{}' failed with {}. Reason: `{}`",
				  ToString(HttpReq.RequestVerb()),
				  HttpReq.QueryString(),
				  static_cast<int>(Result.first),
				  Result.second);
	}
	if (Result.second.empty())
	{
		return HttpReq.WriteResponse(Result.first);
	}
	return HttpReq.WriteResponse(Result.first, HttpContentType::kText, Result.second);
}

void
HttpProjectService::HandleChunkInfoRequest(HttpRouterRequest& Req)
{
	ZEN_TRACE_CPU("ProjectService::ChunkInfo");

	HttpServerRequest& HttpReq = Req.ServerRequest();

	const auto& ProjectId = Req.GetCapture(1);
	const auto& OplogId	  = Req.GetCapture(2);
	const auto& ChunkId	  = Req.GetCapture(3);

	CbObject								 ResponsePayload;
	std::pair<HttpResponseCode, std::string> Result = m_ProjectStore->GetChunkInfo(ProjectId, OplogId, ChunkId, ResponsePayload);
	if (Result.first == HttpResponseCode::OK)
	{
		m_ProjectStats.ChunkHitCount++;
		return HttpReq.WriteResponse(HttpResponseCode::OK, ResponsePayload);
	}
	else if (Result.first == HttpResponseCode::NotFound)
	{
		m_ProjectStats.ChunkMissCount++;
		ZEN_DEBUG("chunk - '{}/{}/{}' MISSING", ProjectId, OplogId, ChunkId);
	}
	else
	{
		if (Result.first == HttpResponseCode::BadRequest)
		{
			m_ProjectStats.BadRequestCount++;
		}
		ZEN_DEBUG("Request {}: '{}' failed with {}. Reason: `{}`",
				  ToString(HttpReq.RequestVerb()),
				  HttpReq.QueryString(),
				  static_cast<int>(Result.first),
				  Result.second);
	}
	if (Result.second.empty())
	{
		return HttpReq.WriteResponse(Result.first);
	}
	return HttpReq.WriteResponse(Result.first, HttpContentType::kText, Result.second);
}

void
HttpProjectService::HandleChunkByIdRequest(HttpRouterRequest& Req)
{
	ZEN_TRACE_CPU("ProjectService::ChunkById");

	HttpServerRequest& HttpReq = Req.ServerRequest();

	const auto& ProjectId = Req.GetCapture(1);
	const auto& OplogId	  = Req.GetCapture(2);
	const auto& ChunkId	  = Req.GetCapture(3);

	uint64_t Offset = 0;
	uint64_t Size	= ~(0ull);

	auto QueryParms = HttpReq.GetQueryParams();

	if (auto OffsetParm = QueryParms.GetValue("offset"); OffsetParm.empty() == false)
	{
		if (auto OffsetVal = ParseInt<uint64_t>(OffsetParm))
		{
			Offset = OffsetVal.value();
		}
		else
		{
			m_ProjectStats.BadRequestCount++;
			return HttpReq.WriteResponse(HttpResponseCode::BadRequest);
		}
	}

	if (auto SizeParm = QueryParms.GetValue("size"); SizeParm.empty() == false)
	{
		if (auto SizeVal = ParseInt<uint64_t>(SizeParm))
		{
			Size = SizeVal.value();
		}
		else
		{
			m_ProjectStats.BadRequestCount++;
			return HttpReq.WriteResponse(HttpResponseCode::BadRequest);
		}
	}

	HttpContentType AcceptType = HttpReq.AcceptContentType();

	CompositeBuffer							 Chunk;
	HttpContentType							 ContentType;
	std::pair<HttpResponseCode, std::string> Result =
		m_ProjectStore->GetChunkRange(ProjectId, OplogId, ChunkId, Offset, Size, AcceptType, Chunk, ContentType);
	if (Result.first == HttpResponseCode::OK)
	{
		m_ProjectStats.ChunkHitCount++;
		ZEN_DEBUG("chunk - '{}/{}/{}' '{}'", ProjectId, OplogId, ChunkId, ToString(ContentType));
		return HttpReq.WriteResponse(HttpResponseCode::OK, ContentType, Chunk);
	}
	else if (Result.first == HttpResponseCode::NotFound)
	{
		m_ProjectStats.ChunkMissCount++;
		ZEN_DEBUG("chunk - '{}/{}/{}' MISSING", ProjectId, OplogId, ChunkId);
	}
	else
	{
		if (Result.first == HttpResponseCode::BadRequest)
		{
			m_ProjectStats.BadRequestCount++;
		}
		ZEN_DEBUG("Request {}: '{}' failed with {}. Reason: `{}`",
				  ToString(HttpReq.RequestVerb()),
				  HttpReq.QueryString(),
				  static_cast<int>(Result.first),
				  Result.second);
	}
	if (Result.second.empty())
	{
		return HttpReq.WriteResponse(Result.first);
	}
	return HttpReq.WriteResponse(Result.first, HttpContentType::kText, Result.second);
}

void
HttpProjectService::HandleChunkByCidRequest(HttpRouterRequest& Req)
{
	ZEN_TRACE_CPU("ProjectService::ChunkByCid");

	HttpServerRequest& HttpReq = Req.ServerRequest();

	const auto&		ProjectId	= Req.GetCapture(1);
	const auto&		OplogId		= Req.GetCapture(2);
	const auto&		Cid			= Req.GetCapture(3);
	HttpContentType AcceptType	= HttpReq.AcceptContentType();
	HttpContentType RequestType = HttpReq.RequestContentType();

	switch (HttpReq.RequestVerb())
	{
		case HttpVerb::kGet:
			{
				IoBuffer								 Value;
				std::pair<HttpResponseCode, std::string> Result = m_ProjectStore->GetChunk(ProjectId, OplogId, Cid, AcceptType, Value);

				if (Result.first == HttpResponseCode::OK)
				{
					m_ProjectStats.ChunkHitCount++;
					return HttpReq.WriteResponse(HttpResponseCode::OK, Value.GetContentType(), Value);
				}
				else if (Result.first == HttpResponseCode::NotFound)
				{
					m_ProjectStats.ChunkMissCount++;
					ZEN_DEBUG("chunk - '{}/{}/{}' MISSING", ProjectId, OplogId, Cid);
				}
				else
				{
					if (Result.first == HttpResponseCode::BadRequest)
					{
						m_ProjectStats.BadRequestCount++;
					}
					ZEN_DEBUG("Request {}: '{}' failed with {}. Reason: `{}`",
							  ToString(HttpReq.RequestVerb()),
							  HttpReq.QueryString(),
							  static_cast<int>(Result.first),
							  Result.second);
				}
				if (Result.second.empty())
				{
					return HttpReq.WriteResponse(Result.first);
				}
				return HttpReq.WriteResponse(Result.first, HttpContentType::kText, Result.second);
			}
		case HttpVerb::kPost:
			{
				if (!m_ProjectStore->AreDiskWritesAllowed())
				{
					return HttpReq.WriteResponse(HttpResponseCode::InsufficientStorage);
				}
				std::pair<HttpResponseCode, std::string> Result =
					m_ProjectStore->PutChunk(ProjectId, OplogId, Cid, RequestType, HttpReq.ReadPayload());
				if (Result.first == HttpResponseCode::OK || Result.first == HttpResponseCode::Created)
				{
					m_ProjectStats.ChunkWriteCount++;
					return HttpReq.WriteResponse(Result.first);
				}
				else
				{
					if (Result.first == HttpResponseCode::BadRequest)
					{
						m_ProjectStats.BadRequestCount++;
					}
					ZEN_DEBUG("Request {}: '{}' failed with {}. Reason: `{}`",
							  ToString(HttpReq.RequestVerb()),
							  HttpReq.QueryString(),
							  static_cast<int>(Result.first),
							  Result.second);
				}
				if (Result.second.empty())
				{
					return HttpReq.WriteResponse(Result.first);
				}
				return HttpReq.WriteResponse(Result.first, HttpContentType::kText, Result.second);
			}
			break;
	}
}

void
HttpProjectService::HandleOplogOpPrepRequest(HttpRouterRequest& Req)
{
	ZEN_TRACE_CPU("ProjectService::OplogOpPrep");

	using namespace std::literals;

	HttpServerRequest& HttpReq = Req.ServerRequest();

	const auto& ProjectId = Req.GetCapture(1);
	const auto& OplogId	  = Req.GetCapture(2);

	Ref<ProjectStore::Project> Project = m_ProjectStore->OpenProject(ProjectId);
	if (!Project)
	{
		return HttpReq.WriteResponse(HttpResponseCode::NotFound);
	}
	Project->TouchProject();

	ProjectStore::Oplog* FoundLog = Project->OpenOplog(OplogId);
	if (!FoundLog)
	{
		return HttpReq.WriteResponse(HttpResponseCode::NotFound);
	}
	Project->TouchOplog(OplogId);

	// This operation takes a list of referenced hashes and decides which
	// chunks are not present on this server. This list is then returned in
	// the "need" list in the response

	IoBuffer Payload	   = HttpReq.ReadPayload();
	CbObject RequestObject = LoadCompactBinaryObject(Payload);

	std::vector<IoHash> NeedList;

	for (auto Entry : RequestObject["have"sv])
	{
		const IoHash FileHash = Entry.AsHash();

		if (!m_CidStore.ContainsChunk(FileHash))
		{
			ZEN_DEBUG("prep - NEED: {}", FileHash);

			NeedList.push_back(FileHash);
		}
	}

	CbObjectWriter Cbo;
	Cbo.BeginArray("need");

	for (const IoHash& Hash : NeedList)
	{
		Cbo << Hash;
	}

	Cbo.EndArray();
	CbObject Response = Cbo.Save();

	return HttpReq.WriteResponse(HttpResponseCode::OK, Response);
}

void
HttpProjectService::HandleOplogOpNewRequest(HttpRouterRequest& Req)
{
	ZEN_TRACE_CPU("ProjectService::OplogOpNew");

	using namespace std::literals;

	HttpServerRequest& HttpReq = Req.ServerRequest();

	if (!m_ProjectStore->AreDiskWritesAllowed())
	{
		return HttpReq.WriteResponse(HttpResponseCode::InsufficientStorage);
	}

	const auto& ProjectId = Req.GetCapture(1);
	const auto& OplogId	  = Req.GetCapture(2);

	HttpServerRequest::QueryParams Params = HttpReq.GetQueryParams();

	bool   IsUsingSalt = false;
	IoHash SaltHash	   = IoHash::Zero;

	if (std::string_view SaltParam = Params.GetValue("salt"sv); SaltParam.empty() == false)
	{
		const uint32_t Salt = std::stoi(std::string(SaltParam));
		SaltHash			= IoHash::HashBuffer(&Salt, sizeof Salt);
		IsUsingSalt			= true;
	}

	Ref<ProjectStore::Project> Project = m_ProjectStore->OpenProject(ProjectId);
	if (!Project)
	{
		return HttpReq.WriteResponse(HttpResponseCode::NotFound);
	}
	Project->TouchProject();

	ProjectStore::Oplog* FoundLog = Project->OpenOplog(OplogId);
	if (!FoundLog)
	{
		return HttpReq.WriteResponse(HttpResponseCode::NotFound);
	}
	Project->TouchOplog(OplogId);

	ProjectStore::Oplog& Oplog = *FoundLog;

	IoBuffer Payload = HttpReq.ReadPayload();

	// This will attempt to open files which may not exist for the case where
	// the prep step rejected the chunk. This should be fixed since there's
	// a performance cost associated with any file system activity

	bool				IsValid = true;
	std::vector<IoHash> MissingChunks;

	CbPackage::AttachmentResolver Resolver = [&](const IoHash& Hash) -> SharedBuffer {
		if (m_CidStore.ContainsChunk(Hash))
		{
			// Return null attachment as we already have it, no point in reading it and storing it again
			return {};
		}

		IoHash AttachmentId;
		if (IsUsingSalt)
		{
			IoHash AttachmentSpec[]{SaltHash, Hash};
			AttachmentId = IoHash::HashBuffer(MakeMemoryView(AttachmentSpec));
		}
		else
		{
			AttachmentId = Hash;
		}

		std::filesystem::path AttachmentPath = Oplog.TempPath() / AttachmentId.ToHexString();
		if (IoBuffer Data = IoBufferBuilder::MakeFromTemporaryFile(AttachmentPath))
		{
			Data.SetDeleteOnClose(true);
			return SharedBuffer(std::move(Data));
		}
		else
		{
			IsValid = false;
			MissingChunks.push_back(Hash);

			return {};
		}
	};

	CbPackage Package;

	if (!legacy::TryLoadCbPackage(Package, Payload, &UniqueBuffer::Alloc, &Resolver))
	{
		if (CbObject Core = LoadCompactBinaryObject(Payload))
		{
			Package.SetObject(Core);
		}
		else
		{
			std::filesystem::path BadPackagePath =
				Oplog.TempPath() / "bad_packages"sv / fmt::format("session{}_request{}"sv, HttpReq.SessionId(), HttpReq.RequestId());

			ZEN_WARN("Received malformed package! Saving payload to '{}'", BadPackagePath);

			WriteFile(BadPackagePath, Payload);

			m_ProjectStats.BadRequestCount++;
			return HttpReq.WriteResponse(HttpResponseCode::BadRequest,
										 HttpContentType::kText,
										 u8"request body must be a compact binary object or package in legacy format");
		}
	}

	m_ProjectStats.ChunkMissCount += MissingChunks.size();

	if (!IsValid)
	{
		ExtendableStringBuilder<256> ResponseText;
		ResponseText.Append("Missing chunk references: ");

		bool IsFirst = true;
		for (const auto& Hash : MissingChunks)
		{
			if (IsFirst)
			{
				IsFirst = false;
			}
			else
			{
				ResponseText.Append(", ");
			}
			Hash.ToHexString(ResponseText);
		}

		return HttpReq.WriteResponse(HttpResponseCode::NotFound, HttpContentType::kText, ResponseText);
	}

	CbObject Core = Package.GetObject();

	if (!Core["key"sv])
	{
		m_ProjectStats.BadRequestCount++;
		return HttpReq.WriteResponse(HttpResponseCode::BadRequest, HttpContentType::kText, "No oplog entry key specified");
	}

	// Write core to oplog

	size_t		   AttachmentCount = Package.GetAttachments().size();
	const uint32_t OpLsn		   = Oplog.AppendNewOplogEntry(Package);

	if (OpLsn == ProjectStore::Oplog::kInvalidOp)
	{
		m_ProjectStats.BadRequestCount++;
		return HttpReq.WriteResponse(HttpResponseCode::BadRequest);
	}
	m_ProjectStats.ChunkWriteCount += AttachmentCount;

	m_ProjectStats.OpWriteCount++;
	ZEN_DEBUG("'{}/{}' op #{} ({}) - '{}'", ProjectId, OplogId, OpLsn, NiceBytes(Payload.Size()), Core["key"sv].AsString());
	HttpReq.WriteResponse(HttpResponseCode::Created);
}

void
HttpProjectService::HandleOpLogOpRequest(HttpRouterRequest& Req)
{
	ZEN_TRACE_CPU("ProjectService::OplogOp");

	HttpServerRequest& HttpReq = Req.ServerRequest();

	const std::string& ProjectId  = Req.GetCapture(1);
	const std::string& OplogId	  = Req.GetCapture(2);
	const std::string& OpIdString = Req.GetCapture(3);

	Ref<ProjectStore::Project> Project = m_ProjectStore->OpenProject(ProjectId);
	if (!Project)
	{
		return HttpReq.WriteResponse(HttpResponseCode::NotFound);
	}
	Project->TouchProject();

	ProjectStore::Oplog* FoundLog = Project->OpenOplog(OplogId);
	if (!FoundLog)
	{
		return HttpReq.WriteResponse(HttpResponseCode::NotFound);
	}
	Project->TouchOplog(OplogId);

	ProjectStore::Oplog& Oplog = *FoundLog;

	if (const std::optional<int32_t> OpId = ParseInt<uint32_t>(OpIdString))
	{
		if (std::optional<CbObject> MaybeOp = Oplog.GetOpByIndex(OpId.value()))
		{
			CbObject& Op = MaybeOp.value();
			if (HttpReq.AcceptContentType() == ZenContentType::kCbPackage)
			{
				CbPackage Package;
				Package.SetObject(Op);

				Op.IterateAttachments([&](CbFieldView FieldView) {
					const IoHash AttachmentHash = FieldView.AsAttachment();
					IoBuffer	 Payload		= m_CidStore.FindChunkByCid(AttachmentHash);
					if (Payload)
					{
						switch (Payload.GetContentType())
						{
							case ZenContentType::kCbObject:
								if (CbObject Object = LoadCompactBinaryObject(Payload))
								{
									Package.AddAttachment(CbAttachment(Object));
								}
								else
								{
									// Error - malformed object

									ZEN_WARN("malformed object returned for {}", AttachmentHash);
								}
								break;

							case ZenContentType::kCompressedBinary:
								if (CompressedBuffer Compressed = CompressedBuffer::FromCompressedNoValidate(std::move(Payload)))
								{
									Package.AddAttachment(CbAttachment(Compressed, AttachmentHash));
								}
								else
								{
									// Error - not compressed!

									ZEN_WARN("invalid compressed binary returned for {}", AttachmentHash);
								}
								break;

							default:
								Package.AddAttachment(CbAttachment(SharedBuffer(Payload)));
								break;
						}
					}
				});
				m_ProjectStats.OpHitCount++;
				return HttpReq.WriteResponse(HttpResponseCode::Accepted, Package);
			}
			else
			{
				// Client cannot accept a package, so we only send the core object
				m_ProjectStats.OpHitCount++;
				return HttpReq.WriteResponse(HttpResponseCode::Accepted, Op);
			}
		}
	}
	m_ProjectStats.OpMissCount++;
	return HttpReq.WriteResponse(HttpResponseCode::NotFound);
}

void
HttpProjectService::HandleOpLogRequest(HttpRouterRequest& Req)
{
	ZEN_TRACE_CPU("ProjectService::Oplog");

	HttpServerRequest& HttpReq = Req.ServerRequest();

	using namespace std::literals;

	const auto& ProjectId = Req.GetCapture(1);
	const auto& OplogId	  = Req.GetCapture(2);

	Ref<ProjectStore::Project> Project = m_ProjectStore->OpenProject(ProjectId);

	if (!Project)
	{
		return HttpReq.WriteResponse(HttpResponseCode::NotFound, HttpContentType::kText, fmt::format("project {} not found", ProjectId));
	}
	Project->TouchProject();

	switch (HttpReq.RequestVerb())
	{
		case HttpVerb::kGet:
			{
				ProjectStore::Oplog* OplogIt = Project->OpenOplog(OplogId);
				if (!OplogIt)
				{
					return HttpReq.WriteResponse(HttpResponseCode::NotFound,
												 HttpContentType::kText,
												 fmt::format("oplog {} not found in project {}", OplogId, ProjectId));
				}
				Project->TouchOplog(OplogId);

				ProjectStore::Oplog& Log = *OplogIt;

				CbObjectWriter Cb;
				Cb << "id"sv << Log.OplogId() << "project"sv << Project->Identifier << "tempdir"sv << Log.TempPath().c_str()
				   << "markerpath"sv << Log.MarkerPath().c_str() << "totalsize"sv << Log.TotalSize() << "opcount" << Log.OplogCount()
				   << "expired"sv << Project->IsExpired(GcClock::TimePoint::min(), Log);

				HttpReq.WriteResponse(HttpResponseCode::OK, Cb.Save());

				m_ProjectStats.OpLogReadCount++;
			}
			break;

		case HttpVerb::kPost:
			{
				if (!m_ProjectStore->AreDiskWritesAllowed())
				{
					return HttpReq.WriteResponse(HttpResponseCode::InsufficientStorage);
				}
				std::filesystem::path OplogMarkerPath;
				if (CbObject Params = HttpReq.ReadPayloadObject())
				{
					OplogMarkerPath = Params["gcpath"sv].AsString();
				}

				ProjectStore::Oplog* OplogIt = Project->OpenOplog(OplogId);
				if (!OplogIt)
				{
					if (!Project->NewOplog(OplogId, OplogMarkerPath))
					{
						// TODO: indicate why the operation failed!
						return HttpReq.WriteResponse(HttpResponseCode::InternalServerError);
					}
					Project->TouchOplog(OplogId);

					m_ProjectStats.OpLogWriteCount++;
					ZEN_INFO("established oplog '{}/{}', gc marker file at '{}'", ProjectId, OplogId, OplogMarkerPath);

					return HttpReq.WriteResponse(HttpResponseCode::Created);
				}

				// I guess this should ultimately be used to execute RPCs but for now, it
				// does absolutely nothing

				m_ProjectStats.BadRequestCount++;
				return HttpReq.WriteResponse(HttpResponseCode::BadRequest);
			}
			break;

		case HttpVerb::kPut:
			{
				if (!m_ProjectStore->AreDiskWritesAllowed())
				{
					return HttpReq.WriteResponse(HttpResponseCode::InsufficientStorage);
				}

				std::filesystem::path OplogMarkerPath;
				if (CbObject Params = HttpReq.ReadPayloadObject())
				{
					OplogMarkerPath = Params["gcpath"sv].AsString();
				}

				ProjectStore::Oplog* FoundLog = Project->OpenOplog(OplogId);
				if (!FoundLog)
				{
					if (!Project->NewOplog(OplogId, OplogMarkerPath))
					{
						// TODO: indicate why the operation failed!
						return HttpReq.WriteResponse(HttpResponseCode::InternalServerError);
					}
					Project->TouchOplog(OplogId);

					m_ProjectStats.OpLogWriteCount++;
					ZEN_INFO("established oplog '{}/{}', gc marker file at '{}'", ProjectId, OplogId, OplogMarkerPath);

					return HttpReq.WriteResponse(HttpResponseCode::Created);
				}
				Project->TouchOplog(OplogId);

				FoundLog->Update(OplogMarkerPath);

				m_ProjectStats.OpLogWriteCount++;
				ZEN_INFO("updated oplog '{}/{}', gc marker file at '{}'", ProjectId, OplogId, OplogMarkerPath);

				return HttpReq.WriteResponse(HttpResponseCode::OK);
			}
			break;

		case HttpVerb::kDelete:
			{
				ZEN_INFO("deleting oplog '{}/{}'", ProjectId, OplogId);

				Project->DeleteOplog(OplogId);

				m_ProjectStats.OpLogDeleteCount++;
				return HttpReq.WriteResponse(HttpResponseCode::OK);
			}
			break;

		default:
			break;
	}
}

void
HttpProjectService::HandleOpLogEntriesRequest(HttpRouterRequest& Req)
{
	ZEN_TRACE_CPU("ProjectService::OplogEntries");

	using namespace std::literals;

	HttpServerRequest& HttpReq = Req.ServerRequest();

	const auto& ProjectId = Req.GetCapture(1);
	const auto& OplogId	  = Req.GetCapture(2);

	Ref<ProjectStore::Project> Project = m_ProjectStore->OpenProject(ProjectId);
	if (!Project)
	{
		return HttpReq.WriteResponse(HttpResponseCode::NotFound);
	}
	Project->TouchProject();

	ProjectStore::Oplog* FoundLog = Project->OpenOplog(OplogId);
	if (!FoundLog)
	{
		return HttpReq.WriteResponse(HttpResponseCode::NotFound);
	}
	Project->TouchOplog(OplogId);

	CbObjectWriter Response;

	if (FoundLog->OplogCount() > 0)
	{
		std::unordered_set<std::string> FieldNamesFilter;
		auto							FilterObject = [&FieldNamesFilter](CbObjectView& Object) -> CbObject {
			   CbObject RewrittenOp = RewriteCbObject(Object, [&FieldNamesFilter](CbObjectWriter&, CbFieldView Field) -> bool {
				   if (FieldNamesFilter.contains(std::string(Field.GetName())))
				   {
					   return false;
				   }

				   return true;
			   });

			   return RewrittenOp;
		};

		HttpServerRequest::QueryParams Params = HttpReq.GetQueryParams();
		if (auto FieldFilter = HttpServerRequest::Decode(Params.GetValue("fieldfilter")); !FieldFilter.empty())
		{
			ForEachStrTok(FieldFilter, ',', [&](std::string_view FieldName) {
				FieldNamesFilter.insert(std::string(FieldName));
				return true;
			});
		}

		if (auto OpKey = Params.GetValue("opkey"); !OpKey.empty())
		{
			Oid						OpKeyId = OpKeyStringAsOId(OpKey);
			std::optional<CbObject> Op		= FoundLog->GetOpByKey(OpKeyId);

			if (Op.has_value())
			{
				if (FieldNamesFilter.empty())
				{
					Response << "entry"sv << Op.value();
				}
				else
				{
					Response << "entry"sv << FilterObject(Op.value());
				}
			}
			else
			{
				return HttpReq.WriteResponse(HttpResponseCode::NotFound);
			}
		}
		else
		{
			Response.BeginArray("entries"sv);

			if (FieldNamesFilter.empty())
			{
				FoundLog->IterateOplog([&Response](CbObjectView Op) { Response << Op; });
			}
			else
			{
				FoundLog->IterateOplog([this, &Response, &FilterObject](CbObjectView Op) { Response << FilterObject(Op); });
			}

			Response.EndArray();
		}
	}
	if (HttpReq.AcceptContentType() == HttpContentType::kCompressedBinary)
	{
		CompositeBuffer Payload = CompressedBuffer::Compress(Response.Save().GetBuffer()).GetCompressed();
		return HttpReq.WriteResponse(HttpResponseCode::OK, HttpContentType::kCompressedBinary, Payload);
	}
	else
	{
		return HttpReq.WriteResponse(HttpResponseCode::OK, Response.Save());
	}
}

void
HttpProjectService::HandleProjectRequest(HttpRouterRequest& Req)
{
	ZEN_TRACE_CPU("ProjectService::Project");

	using namespace std::literals;

	HttpServerRequest& HttpReq	 = Req.ServerRequest();
	const std::string  ProjectId = Req.GetCapture(1);

	switch (HttpReq.RequestVerb())
	{
		case HttpVerb::kPost:
			{
				if (!m_ProjectStore->AreDiskWritesAllowed())
				{
					return HttpReq.WriteResponse(HttpResponseCode::InsufficientStorage);
				}

				IoBuffer			  Payload	 = HttpReq.ReadPayload();
				CbObject			  Params	 = LoadCompactBinaryObject(Payload);
				std::filesystem::path Root		 = Params["root"sv].AsU8String();	 // Workspace root (i.e `D:/UE5/`)
				std::filesystem::path EngineRoot = Params["engine"sv].AsU8String();	 // Engine root (i.e `D:/UE5/Engine`)
				std::filesystem::path ProjectRoot =
					Params["project"sv].AsU8String();  // Project root directory (i.e `D:/UE5/Samples/Games/Lyra`)
				std::filesystem::path ProjectFilePath =
					Params["projectfile"sv].AsU8String();  // Project file path (i.e `D:/UE5/Samples/Games/Lyra/Lyra.uproject`)

				const std::filesystem::path BasePath = m_ProjectStore->BasePath() / ProjectId;
				m_ProjectStore->NewProject(BasePath, ProjectId, Root, EngineRoot, ProjectRoot, ProjectFilePath);

				ZEN_INFO("established project (id: '{}', roots: '{}', '{}', '{}', '{}'{})",
						 ProjectId,
						 Root,
						 EngineRoot,
						 ProjectRoot,
						 ProjectFilePath,
						 ProjectFilePath.empty() ? ", project will not be GCd due to empty project file path" : "");

				m_ProjectStats.ProjectWriteCount++;
				HttpReq.WriteResponse(HttpResponseCode::Created);
			}
			break;

		case HttpVerb::kPut:
			{
				if (!m_ProjectStore->AreDiskWritesAllowed())
				{
					return HttpReq.WriteResponse(HttpResponseCode::InsufficientStorage);
				}

				IoBuffer			  Payload	 = HttpReq.ReadPayload();
				CbObject			  Params	 = LoadCompactBinaryObject(Payload);
				std::filesystem::path Root		 = Params["root"sv].AsU8String();	 // Workspace root (i.e `D:/UE5/`)
				std::filesystem::path EngineRoot = Params["engine"sv].AsU8String();	 // Engine root (i.e `D:/UE5/Engine`)
				std::filesystem::path ProjectRoot =
					Params["project"sv].AsU8String();  // Project root directory (i.e `D:/UE5/Samples/Games/Lyra`)
				std::filesystem::path ProjectFilePath =
					Params["projectfile"sv].AsU8String();  // Project file path (i.e `D:/UE5/Samples/Games/Lyra/Lyra.uproject`)

				if (m_ProjectStore->UpdateProject(ProjectId, Root, EngineRoot, ProjectRoot, ProjectFilePath))
				{
					m_ProjectStats.ProjectWriteCount++;
					ZEN_INFO("updated project (id: '{}', roots: '{}', '{}', '{}', '{}'{})",
							 ProjectId,
							 Root,
							 EngineRoot,
							 ProjectRoot,
							 ProjectFilePath,
							 ProjectFilePath.empty() ? ", project will not be GCd due to empty project file path" : "");

					HttpReq.WriteResponse(HttpResponseCode::OK);
				}
				else
				{
					const std::filesystem::path BasePath = m_ProjectStore->BasePath() / ProjectId;
					m_ProjectStore->NewProject(BasePath, ProjectId, Root, EngineRoot, ProjectRoot, ProjectFilePath);

					m_ProjectStats.ProjectWriteCount++;
					ZEN_INFO("established project (id: '{}', roots: '{}', '{}', '{}', '{}'{})",
							 ProjectId,
							 Root,
							 EngineRoot,
							 ProjectRoot,
							 ProjectFilePath,
							 ProjectFilePath.empty() ? ", project will not be GCd due to empty project file path" : "");

					HttpReq.WriteResponse(HttpResponseCode::Created);
				}
			}
			break;

		case HttpVerb::kGet:
			{
				Ref<ProjectStore::Project> Project = m_ProjectStore->OpenProject(ProjectId);
				if (!Project)
				{
					return HttpReq.WriteResponse(HttpResponseCode::NotFound,
												 HttpContentType::kText,
												 fmt::format("project {} not found", ProjectId));
				}
				Project->TouchProject();

				std::vector<std::string> OpLogs = Project->ScanForOplogs();

				CbObjectWriter Response;
				Response << "id"sv << Project->Identifier;
				Response << "root"sv << PathToUtf8(Project->RootDir);
				Response << "engine"sv << PathToUtf8(Project->EngineRootDir);
				Response << "project"sv << PathToUtf8(Project->ProjectRootDir);
				Response << "projectfile"sv << PathToUtf8(Project->ProjectFilePath);

				Response.BeginArray("oplogs"sv);
				for (const std::string& OplogId : OpLogs)
				{
					Response.BeginObject();
					Response << "id"sv << OplogId;
					Response.EndObject();
				}
				Response.EndArray();  // oplogs

				HttpReq.WriteResponse(HttpResponseCode::OK, Response.Save());

				m_ProjectStats.ProjectReadCount++;
			}
			break;

		case HttpVerb::kDelete:
			{
				Ref<ProjectStore::Project> Project = m_ProjectStore->OpenProject(ProjectId);
				if (!Project)
				{
					return HttpReq.WriteResponse(HttpResponseCode::NotFound,
												 HttpContentType::kText,
												 fmt::format("project {} not found", ProjectId));
				}

				ZEN_INFO("deleting project '{}'", ProjectId);
				if (!m_ProjectStore->DeleteProject(ProjectId))
				{
					return HttpReq.WriteResponse(HttpResponseCode::Locked,
												 HttpContentType::kText,
												 fmt::format("project {} is in use", ProjectId));
				}

				m_ProjectStats.ProjectDeleteCount++;
				return HttpReq.WriteResponse(HttpResponseCode::NoContent);
			}
			break;

		default:
			break;
	}
}

void
HttpProjectService::HandleOplogSaveRequest(HttpRouterRequest& Req)
{
	ZEN_TRACE_CPU("ProjectService::OplogSave");

	HttpServerRequest& HttpReq = Req.ServerRequest();

	if (!m_ProjectStore->AreDiskWritesAllowed())
	{
		return HttpReq.WriteResponse(HttpResponseCode::InsufficientStorage);
	}

	const auto& ProjectId = Req.GetCapture(1);
	const auto& OplogId	  = Req.GetCapture(2);
	if (HttpReq.RequestContentType() != HttpContentType::kCbObject)
	{
		m_ProjectStats.BadRequestCount++;
		return HttpReq.WriteResponse(HttpResponseCode::BadRequest, HttpContentType::kText, "Invalid content type");
	}
	IoBuffer Payload = HttpReq.ReadPayload();

	CbObject								 Response;
	std::pair<HttpResponseCode, std::string> Result = m_ProjectStore->WriteOplog(ProjectId, OplogId, std::move(Payload), Response);
	if (Result.first == HttpResponseCode::OK)
	{
		return HttpReq.WriteResponse(HttpResponseCode::OK, Response);
	}
	else
	{
		if (Result.first == HttpResponseCode::BadRequest)
		{
			m_ProjectStats.BadRequestCount++;
		}
		ZEN_DEBUG("Request {}: '{}' failed with {}. Reason: `{}`",
				  ToString(HttpReq.RequestVerb()),
				  HttpReq.QueryString(),
				  static_cast<int>(Result.first),
				  Result.second);
	}
	if (Result.second.empty())
	{
		return HttpReq.WriteResponse(Result.first);
	}
	return HttpReq.WriteResponse(Result.first, HttpContentType::kText, Result.second);
}

void
HttpProjectService::HandleOplogLoadRequest(HttpRouterRequest& Req)
{
	ZEN_TRACE_CPU("ProjectService::OplogLoad");

	HttpServerRequest& HttpReq	 = Req.ServerRequest();
	const auto&		   ProjectId = Req.GetCapture(1);
	const auto&		   OplogId	 = Req.GetCapture(2);
	if (HttpReq.AcceptContentType() != HttpContentType::kCbObject)
	{
		m_ProjectStats.BadRequestCount++;
		return HttpReq.WriteResponse(HttpResponseCode::BadRequest, HttpContentType::kText, "Invalid accept content type");
	}
	IoBuffer Payload = HttpReq.ReadPayload();

	CbObject								 Response;
	std::pair<HttpResponseCode, std::string> Result = m_ProjectStore->ReadOplog(ProjectId, OplogId, HttpReq.GetQueryParams(), Response);
	if (Result.first == HttpResponseCode::OK)
	{
		return HttpReq.WriteResponse(HttpResponseCode::OK, Response);
	}
	else
	{
		if (Result.first == HttpResponseCode::BadRequest)
		{
			m_ProjectStats.BadRequestCount++;
		}
		ZEN_DEBUG("Request {}: '{}' failed with {}. Reason: `{}`",
				  ToString(HttpReq.RequestVerb()),
				  HttpReq.QueryString(),
				  static_cast<int>(Result.first),
				  Result.second);
	}
	if (Result.second.empty())
	{
		return HttpReq.WriteResponse(Result.first);
	}
	return HttpReq.WriteResponse(Result.first, HttpContentType::kText, Result.second);
}

void
HttpProjectService::HandleRpcRequest(HttpRouterRequest& Req)
{
	ZEN_TRACE_CPU("ProjectService::Rpc");

	HttpServerRequest& HttpReq = Req.ServerRequest();

	const auto& ProjectId = Req.GetCapture(1);
	const auto& OplogId	  = Req.GetCapture(2);
	IoBuffer	Payload	  = HttpReq.ReadPayload();

	bool OkRequest = m_ProjectStore->Rpc(HttpReq, ProjectId, OplogId, std::move(Payload), m_AuthMgr);
	if (!OkRequest)
	{
		m_ProjectStats.BadRequestCount++;
	}
}
void
HttpProjectService::HandleDetailsRequest(HttpRouterRequest& Req)
{
	ZEN_TRACE_CPU("ProjectService::Details");

	using namespace std::literals;

	HttpServerRequest& HttpReq = Req.ServerRequest();

	HttpServerRequest::QueryParams Params			 = HttpReq.GetQueryParams();
	bool						   CSV				 = Params.GetValue("csv"sv) == "true"sv;
	bool						   Details			 = Params.GetValue("details"sv) == "true"sv;
	bool						   OpDetails		 = Params.GetValue("opdetails"sv) == "true"sv;
	bool						   AttachmentDetails = Params.GetValue("attachmentdetails"sv) == "true"sv;

	if (CSV)
	{
		ExtendableStringBuilder<4096> CSVWriter;
		CSVHeader(Details, AttachmentDetails, CSVWriter);

		m_ProjectStore->IterateProjects([&](ProjectStore::Project& Project) {
			Project.IterateOplogs([&](const RwLock::SharedLockScope&, ProjectStore::Oplog& Oplog) {
				Oplog.IterateOplogWithKey(
					[this, &Project, &Oplog, &CSVWriter, Details, AttachmentDetails](int LSN, const Oid& Key, CbObjectView Op) {
						CSVWriteOp(m_CidStore, Project.Identifier, Oplog.OplogId(), Details, AttachmentDetails, LSN, Key, Op, CSVWriter);
					});
			});
		});

		HttpReq.WriteResponse(HttpResponseCode::OK, HttpContentType::kText, CSVWriter.ToView());
	}
	else
	{
		CbObjectWriter Cbo;
		Cbo.BeginArray("projects");
		{
			m_ProjectStore->DiscoverProjects();

			m_ProjectStore->IterateProjects([&](ProjectStore::Project& Project) {
				std::vector<std::string> OpLogs = Project.ScanForOplogs();
				CbWriteProject(m_CidStore, Project, OpLogs, Details, OpDetails, AttachmentDetails, Cbo);
			});
		}
		Cbo.EndArray();
		HttpReq.WriteResponse(HttpResponseCode::OK, Cbo.Save());
	}
}

void
HttpProjectService::HandleProjectDetailsRequest(HttpRouterRequest& Req)
{
	ZEN_TRACE_CPU("ProjectService::ProjectDetails");

	using namespace std::literals;

	HttpServerRequest& HttpReq	 = Req.ServerRequest();
	const auto&		   ProjectId = Req.GetCapture(1);

	HttpServerRequest::QueryParams Params			 = HttpReq.GetQueryParams();
	bool						   CSV				 = Params.GetValue("csv"sv) == "true"sv;
	bool						   Details			 = Params.GetValue("details"sv) == "true"sv;
	bool						   OpDetails		 = Params.GetValue("opdetails"sv) == "true"sv;
	bool						   AttachmentDetails = Params.GetValue("attachmentdetails"sv) == "true"sv;

	Ref<ProjectStore::Project> FoundProject = m_ProjectStore->OpenProject(ProjectId);
	if (!FoundProject)
	{
		return HttpReq.WriteResponse(HttpResponseCode::NotFound);
	}
	ProjectStore::Project& Project = *FoundProject.Get();

	if (CSV)
	{
		ExtendableStringBuilder<4096> CSVWriter;
		CSVHeader(Details, AttachmentDetails, CSVWriter);

		FoundProject->IterateOplogs([&](const RwLock::SharedLockScope&, ProjectStore::Oplog& Oplog) {
			Oplog.IterateOplogWithKey(
				[this, &Project, &Oplog, &CSVWriter, Details, AttachmentDetails](int LSN, const Oid& Key, CbObjectView Op) {
					CSVWriteOp(m_CidStore, Project.Identifier, Oplog.OplogId(), Details, AttachmentDetails, LSN, Key, Op, CSVWriter);
				});
		});
		HttpReq.WriteResponse(HttpResponseCode::OK, HttpContentType::kText, CSVWriter.ToView());
	}
	else
	{
		CbObjectWriter			 Cbo;
		std::vector<std::string> OpLogs = FoundProject->ScanForOplogs();
		Cbo.BeginArray("projects");
		{
			CbWriteProject(m_CidStore, Project, OpLogs, Details, OpDetails, AttachmentDetails, Cbo);
		}
		Cbo.EndArray();
		HttpReq.WriteResponse(HttpResponseCode::OK, Cbo.Save());
	}
}

void
HttpProjectService::HandleOplogDetailsRequest(HttpRouterRequest& Req)
{
	ZEN_TRACE_CPU("ProjectService::OplogDetails");

	using namespace std::literals;

	HttpServerRequest& HttpReq	 = Req.ServerRequest();
	const auto&		   ProjectId = Req.GetCapture(1);
	const auto&		   OplogId	 = Req.GetCapture(2);

	HttpServerRequest::QueryParams Params			 = HttpReq.GetQueryParams();
	bool						   CSV				 = Params.GetValue("csv"sv) == "true"sv;
	bool						   Details			 = Params.GetValue("details"sv) == "true"sv;
	bool						   OpDetails		 = Params.GetValue("opdetails"sv) == "true"sv;
	bool						   AttachmentDetails = Params.GetValue("attachmentdetails"sv) == "true"sv;

	Ref<ProjectStore::Project> FoundProject = m_ProjectStore->OpenProject(ProjectId);
	if (!FoundProject)
	{
		return HttpReq.WriteResponse(HttpResponseCode::NotFound);
	}

	ProjectStore::Oplog* FoundLog = FoundProject->OpenOplog(OplogId);
	if (!FoundLog)
	{
		return HttpReq.WriteResponse(HttpResponseCode::NotFound);
	}

	ProjectStore::Project& Project = *FoundProject.Get();
	ProjectStore::Oplog&   Oplog   = *FoundLog;
	if (CSV)
	{
		ExtendableStringBuilder<4096> CSVWriter;
		CSVHeader(Details, AttachmentDetails, CSVWriter);

		Oplog.IterateOplogWithKey(
			[this, &Project, &Oplog, &CSVWriter, Details, AttachmentDetails](int LSN, const Oid& Key, CbObjectView Op) {
				CSVWriteOp(m_CidStore, Project.Identifier, Oplog.OplogId(), Details, AttachmentDetails, LSN, Key, Op, CSVWriter);
			});
		HttpReq.WriteResponse(HttpResponseCode::OK, HttpContentType::kText, CSVWriter.ToView());
	}
	else
	{
		CbObjectWriter Cbo;
		Cbo.BeginArray("oplogs");
		{
			CbWriteOplog(m_CidStore, Oplog, Details, OpDetails, AttachmentDetails, Cbo);
		}
		Cbo.EndArray();
		HttpReq.WriteResponse(HttpResponseCode::OK, Cbo.Save());
	}
}

void
HttpProjectService::HandleOplogOpDetailsRequest(HttpRouterRequest& Req)
{
	ZEN_TRACE_CPU("ProjectService::OplogOpDetails");

	using namespace std::literals;

	HttpServerRequest& HttpReq	 = Req.ServerRequest();
	const auto&		   ProjectId = Req.GetCapture(1);
	const auto&		   OplogId	 = Req.GetCapture(2);
	const auto&		   OpId		 = Req.GetCapture(3);

	HttpServerRequest::QueryParams Params			 = HttpReq.GetQueryParams();
	bool						   CSV				 = Params.GetValue("csv"sv) == "true"sv;
	bool						   Details			 = Params.GetValue("details"sv) == "true"sv;
	bool						   OpDetails		 = Params.GetValue("opdetails"sv) == "true"sv;
	bool						   AttachmentDetails = Params.GetValue("attachmentdetails"sv) == "true"sv;

	Ref<ProjectStore::Project> FoundProject = m_ProjectStore->OpenProject(ProjectId);
	if (!FoundProject)
	{
		return HttpReq.WriteResponse(HttpResponseCode::NotFound);
	}

	ProjectStore::Oplog* FoundLog = FoundProject->OpenOplog(OplogId);
	if (!FoundLog)
	{
		return HttpReq.WriteResponse(HttpResponseCode::NotFound);
	}

	if (OpId.size() != 2 * sizeof(Oid::OidBits))
	{
		m_ProjectStats.BadRequestCount++;
		return HttpReq.WriteResponse(HttpResponseCode::BadRequest,
									 HttpContentType::kText,
									 fmt::format("Chunk info request for invalid chunk id '{}/{}'/'{}'", ProjectId, OplogId, OpId));
	}

	const Oid			   ObjId   = Oid::FromHexString(OpId);
	ProjectStore::Project& Project = *FoundProject.Get();
	ProjectStore::Oplog&   Oplog   = *FoundLog;

	int LSN = Oplog.GetOpIndexByKey(ObjId);
	if (LSN == -1)
	{
		return HttpReq.WriteResponse(HttpResponseCode::NotFound);
	}
	std::optional<CbObject> Op = Oplog.GetOpByIndex(LSN);
	if (!Op.has_value())
	{
		return HttpReq.WriteResponse(HttpResponseCode::NotFound);
	}

	if (CSV)
	{
		ExtendableStringBuilder<4096> CSVWriter;
		CSVHeader(Details, AttachmentDetails, CSVWriter);

		CSVWriteOp(m_CidStore, Project.Identifier, Oplog.OplogId(), Details, AttachmentDetails, LSN, ObjId, Op.value(), CSVWriter);
		HttpReq.WriteResponse(HttpResponseCode::OK, HttpContentType::kText, CSVWriter.ToView());
	}
	else
	{
		CbObjectWriter Cbo;
		Cbo.BeginArray("ops");
		{
			CbWriteOp(m_CidStore, Details, OpDetails, AttachmentDetails, LSN, ObjId, Op.value(), Cbo);
		}
		Cbo.EndArray();
		HttpReq.WriteResponse(HttpResponseCode::OK, Cbo.Save());
	}
}

}  // namespace zen
